06. Prototypal Inheritance: Subclasses

Subclasses

One of the benefits of implementing inheritance is that it allows you to reuse existing code. By establishing inheritance, we can subclass, that is, have a "child" object take on most or all of a "parent" object's properties while retaining unique properties of its own.

Let's say we have a parent Animal object, which contains properties like age and weight. That same Animal object can also access methods like eat and sleep.

Now, let's also say that we want to create a Cat child object. Just like you can with other animals, you can also describe a cat by its age or weight, and you can also be certain that the cat eats and sleeps as well. When creating that Cat object, then, we can simply re-write and re-implement all those methods and properties from Animal -- or, we can save some time and prevent repeated code by having Cat inherit those existing properties and methods from Animal!

Not only can Cat take on properties and methods of Animal, we can also give Cat its own unique properties and methods as well! Perhaps a Cat has a unique lives property of 9, or it has a specialized meow() method that no Animal has.

By using prototypal inheritance, Cat only needs to implement Cat-specific functionality, and just reuse Animal's existing functionality.

Inheritance Via Prototypes

Recall the prototype chain from the previous section:

_The `Cat()` constructor function is invoked using the `new` operator, which creates the `bailey` instance (object). Note that the `meow()` method is defined in the prototype of the `bailey` object's constructor function. The prototype is just an object, and all objects created by that constructor are secretly linked to the prototype. As such, we can execute `bailey.meow()` as if it were `bailey`'s own method!_

The Cat() constructor function is invoked using the new operator, which creates the bailey instance (object). Note that the meow() method is defined in the prototype of the bailey object's constructor function. The prototype is just an object, and all objects created by that constructor are secretly linked to the prototype. As such, we can execute bailey.meow() as if it were bailey's own method!

When calling any property on any object, the JavaScript engine will first look for the property in the object itself (i.e., the object's own, non-inherited properties). If the property is not found, JavaScript will then look at the object's prototype. If the property still isn't found in the object's prototype, JavaScript will continue the search up the prototype chain.

Again, inheritance in JavaScript is all about setting up this chain!

As you know, an object's constructor function's prototype is first place searched when the JavaScript engine tries to access a property that doesn't exist in the object itself. Consider the following bear object with two properties, claws and diet:

const bear = {
  claws: true,
  diet: 'carnivore'
};

We'll assign the following PolarBear() constructor function's prototype property to bear:

function PolarBear() { 
  // ...
}

PolarBear.prototype = bear;

Let's now call the PolarBear() constructor to create a new object, then give it two properties:

const snowball = new PolarBear();

snowball.color = 'white';
snowball.favoriteDrink = 'cola';

This is how the snowball object looks at this point:

{
  color: 'white',
  favoriteDrink: 'cola'
}

Note that snowball has just two properties of its own: color and favoriteDrink. However, snowball also has access to properties that don't exist inside it: claws and diet:

console.log(snowball.claws);
// true

console.log(snowball.diet);
// 'carnivore'

Since claws and diet both exist as properties in the prototype object, they are looked up because objects are secretly linked to their constructor's prototype property.

Great! But you may be wondering: just what is this secret link that leads to the prototype object? Right after objects are made from the PolarBear() constructor (such as snowball), they have immediate access to properties in PolarBear()'s prototype. How exactly is this possible?

As it turns out, the secret link is snowball's __proto__ property (note the two underscores on each end). __proto__ is a property of all objects (i.e., instances) made by a constructor function, and points directly to that constructor's prototype object. Let's check out what it looks like!

console.log(snowball.__proto__);

// { claws: true, diet: 'carnivore' }

Since the __proto__ property refers to the same object as PolarBear's prototype, bear, comparing them returns true:

console.log(snowball.__proto__ === bear);

// true

It is highly discouraged to reassign the __proto__ property, or even use it in any code you write. First, there are compatibility issues across browsers. What's more: since the JavaScript engine searches and accesses properties along the prototype chain, mutating an object's prototype can lead to performance issues. The MDN article for proto even warns against using this property in red text at the very top of the page!

It's great to know the secret link for learning how functions and objects are interconnected, but you should not use __proto__ to manage inheritance. If you ever just need to review an object's prototype, you can still use Object.getPrototypeOf().

💡 What About Just Inheriting the Prototype? 💡

Let's say we want a Child object to inherit from a Parent object. Why shouldn't we just set Child.prototype = Parent.prototype?

First, recall that objects are passed by reference. This means that since the Child.prototype object and the Parent.prototype object refer to the same object -- any changes you make to Child's prototype will also be made to Parent's prototype! We don't want children being able to modify properties of their parents!

On top of all this, no prototype chain will be set up. What if we want an object to inherit from any object we want, not just its prototype?

We still need a way to efficiently manage inheritance without mutating the prototype at all.

Consider the following:

function GuineaPig (name) {
  this.name = name;
  this.isCute = true;
}

const waffle = new GuineaPig('Waffle');

What does waffle.__proto__ refer to?

SOLUTION: `GuineaPig.prototype`

QUIZ QUESTION::

Consider the following:

function Car (color, year) {
  this.color = color;
  this.year = year;
}

Car.prototype.drive = function () {
  console.log('Vroom vroom!');
};

const car = new Car('silver', 1988);

What happens when car.drive(); is executed? List the following events in the order that they occur:

ANSWER CHOICES:



Order

Event

Finally, since drive is invoked as a method on car, the value of this is set to car.

Because Car.prototype.drive is a defined property, it is returned.

The JavaScript engine does not find drive within the car object.

Since the car.__proto__ property points to Car.prototype, the JavaScript engine searches for drive in the prototype.

The JavaScript engine then accesses the car.__proto__ property.

First, the JavaScript engine searches inside the car object for a property named drive.

SOLUTION:

Order

Event

Finally, since drive is invoked as a method on car, the value of this is set to car.

Because Car.prototype.drive is a defined property, it is returned.

The JavaScript engine does not find drive within the car object.

Since the car.__proto__ property points to Car.prototype, the JavaScript engine searches for drive in the prototype.

The JavaScript engine then accesses the car.__proto__ property.

First, the JavaScript engine searches inside the car object for a property named drive.

Object.create()

At this point, we've reached a few roadblocks when it comes to inheritance. First, even though __proto__ can access the prototype of the object it is called on, using it in any code you write is not good practice.

What's more: we also shouldn't inherit only the prototype; this doesn't set up the prototype chain, and any changes that we made to a child object will also be reflected in a parent object.

So how should we move forward?

There's actually a way for us to set up the prototype of an object ourselves: using Object.create(). And best of all, this approach lets us manage inheritance without altering the prototype!

Object.create() takes in a single object as an argument, and returns a new object with its __proto__ property set to what argument is passed into it. From that point, you simply set the returned object to be the prototype of the child object's constructor function. Let's check out an example!

First, let's say we have a mammal object with two properties: vertebrate and earBones:

const mammal = {
  vertebrate: true,
  earBones: 3
};

Recall that Object.create() takes in a single object as an argument, and returns a new object. That new object's __proto__ property is set to whatever was originally passed into Object.create(). Let's save that returned value to a variable, rabbit:

const rabbit = Object.create(mammal);

We expect the new rabbit object to be blank, with no properties of its own:

console.log(rabbit);

// {}

However, rabbit should now be secretly linked to mammal. That is, its __proto__ property should point to mammal:

console.log(rabbit.__proto__ === mammal);

// true

Great! This means that now, rabbit extends mammal (i.e., rabbit inherits from mammal). As a result, rabbit can access mammal's properties as if it were its own!

console.log(rabbit.vertebrate);
// true

console.log(rabbit.earBones);
// 3

Object.create() gives us a clean method of establishing prototypal inheritance in JavaScript. We can easily extend the prototype chain this way, and we can have objects inherit from just about any object we want!

Let's check out a more involved example below:

L3 - 88 - Object.Create Demo

Consider the following:

function Parent() {
  // ...
}

function Child() {
  // ...
}

Child.prototype = Object.create(Parent.prototype);

const child = new Child();

The following is then executed:

child instanceof Parent;

What is printed to the console?

SOLUTION: `true`

What is true about Object.create()? Select all that apply:

SOLUTION:
  • It returns a new object whose `__proto__` property is set to the object passed into `Object.create()`
  • Using `Object.create()`, we can have objects inherit from just about any object we want (i.e., not only the `prototype`)
  • `Object.create()` allows us to implement prototypal inheritance without mutating the prototype

Summary

Inheritance in JavaScript is all about setting up the prototype chain. This allows us to subclass, that is, create a "child" object that inherits most or all of a "parent" object's properties and methods. We can then implement any of the child object's unique properties and methods separately, while still retaining data and functionality from its parent.

An object (instance) is secretly linked to its constructor function's prototype object through that instance's __proto__ property. You should never use the __proto__ property in any code you write. Using __proto__ in any code, or even inheriting just the prototype directly, leads to some unwanted side effects.

To efficiently manage inheritance in JavaScript, an effective approach is to avoid mutating the prototype completely. Object.create() allows us to do just that, taking in a parent object and returning a new object with its __proto__ property set to that parent object.

Further Research